﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace T4ViewModel
{
    public interface IViewModelSetup
    {
        string Name { get; set; }
        string GenerateCode();
    }

    public class ViewModelSetup<ModelType> : IViewModelSetup
        where ModelType : new()
    {
        private List<PropertyInfo> properties = new List<PropertyInfo>();
        private List<AddeddPropertyInfo> addedProperties = new List<AddeddPropertyInfo>();
        public string Name { get; set; }


        /// <summary>
        /// Includes specific property from model to view model
        /// </summary>
        /// <typeparam name="PropertyType">Type of property to be included</typeparam>
        /// <param name="propertyDefinition">Lambda expression for selected model property</param>
        /// <returns></returns>
        public ViewModelSetup<ModelType> IncludeProperty<PropertyType>(Expression<Func<ModelType, PropertyType>> propertyDefinition)
        {
            var propertyName = ExpressionParser.GetPropertyName(propertyDefinition);
            var modelProperty = typeof(ModelType).GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
            properties.Add(modelProperty);
            return this;
        }

        /// <summary>
        /// Includes all properties from model to view model
        /// </summary>
        /// <returns></returns>
        public ViewModelSetup<ModelType> IncludeAllProperties()
        {
            var modelProperties = typeof(ModelType).GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (var modelProperty in modelProperties)
                properties.Add(modelProperty);
            return this;
        }

        /// <summary>
        ///  Excludes specific model property from view model
        /// </summary>
        /// <typeparam name="PropertyType">Type of property to be excluded</typeparam>
        /// <param name="propertyDefinition">Lambda expression for selected model property</param>
        /// <returns></returns>
        public ViewModelSetup<ModelType> ExcludeProperty<PropertyType>(Expression<Func<ModelType, PropertyType>> propertyDefinition)
        {
            var propertyName = ExpressionParser.GetPropertyName(propertyDefinition);
            properties.RemoveAll(p => p.Name == propertyName);
            return this;
        }

        /// <summary>
        /// Adds new property to view model
        /// </summary>
        /// <typeparam name="PropertyType">Type of property to be added</typeparam>
        /// <param name="propertyName">Name for new property</param>
        /// <param name="customAttributes">Optional list of attributes that new property will be decorated with. List should contains expressions containing attribute constructors.</param>
        /// <returns></returns>
        public ViewModelSetup<ModelType> AddProperty<PropertyType>(string propertyName, params Expression<Func<Attribute>>[] customAttributes)
        {
            var attributes = new List<AddedAttributeInfo>();

            foreach (var attributeFunc in customAttributes)
            {
                NewExpression newExpression;
                
                if(attributeFunc.Body.GetType() == typeof(MemberInitExpression))
                    newExpression = ((MemberInitExpression)attributeFunc.Body).NewExpression;
                else if(attributeFunc.Body.GetType() == typeof(NewExpression))
                    newExpression = (NewExpression)attributeFunc.Body;
                else
                    throw new ArgumentOutOfRangeException("Attribute expression type");

                var memberInitArguments = newExpression.Arguments;
                var constructorParams = memberInitArguments.Cast<ConstantExpression>().Select(ce => ToCodeString(ce.Value));
                attributes.Add(new AddedAttributeInfo() { Attribute = attributeFunc.Compile().Invoke(), ConstuctorParams = constructorParams });
            }

            addedProperties.Add(new AddeddPropertyInfo() { Name = propertyName, Type = typeof(PropertyType), CustomAttributes = attributes });
            return this;
        }

        private string GenerateAttributeCode(AddedAttributeInfo attributeInfo)
        {
            var attributeType = attributeInfo.Attribute.GetType();
            var code = "[" + GetFullName(attributeType) + "({0})]";
            var props = new List<string>();

            foreach (var constuctorParam in attributeInfo.ConstuctorParams)
                props.Add(constuctorParam);

            foreach (var property in attributeType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (property.GetSetMethod(false) == null)
                    continue;

                var attributeValue = property.GetValue(attributeInfo.Attribute);
                var propertyType = property.PropertyType;

                object defaultValue;
                if (propertyType.IsValueType)
                    defaultValue = Activator.CreateInstance(propertyType);
                if (propertyType == typeof(Type))
                    defaultValue = null;
                else if (propertyType == typeof(string))
                    defaultValue = null;
                else
                    continue;


                if (ToCodeString(defaultValue) != ToCodeString(attributeValue))
                {
                    props.Add(property.Name + " = " + ToCodeString(attributeValue));
                }
            }

            return string.Format(code, string.Join(", ", props));
        }

        private string ToCodeString(object obj)
        {
            if (obj == null)
                return "null";

            if (obj is Type)
                return "typeof(" + obj + ")";

            if (obj.GetType() == typeof(string))
                return "\"" + obj.ToString() + "\"";

            if (obj.GetType().IsEnum)
                return obj.GetType().FullName + "." + obj.ToString();

            return obj.ToString();
        }


        private string Indent(string code, int indent = 1)
        {
            if (indent == 0)
                return code;

            var indentString = "    ";
            var lines = code.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
            for (var i = 0; i < lines.Length; i++)
                lines[i] = indentString + lines[i];

            return Indent(string.Join(Environment.NewLine, lines), indent - 1);
        }

        public string GenerateCode()
        {
            var modelType = typeof(ModelType);
            var modelToViewModel = new StringBuilder();
            var viewModelToModel = new StringBuilder();
            var viewModelToModelWithIgnore = new StringBuilder();
            var viewModelProperties = new StringBuilder();

            foreach (var property in properties)
            {
                foreach (var customAttribute in property.CustomAttributes)
                {
                    var constructorParams = customAttribute.ConstructorArguments
                        .Select(ca => ToCodeString(ca.ArgumentType.IsEnum ? Enum.ToObject(ca.ArgumentType, ca.Value) : ca.Value));

                    var attributeInstance = Attribute.GetCustomAttribute(property, customAttribute.AttributeType);
                    viewModelProperties.AppendLine(GenerateAttributeCode(new AddedAttributeInfo() { Attribute = attributeInstance, ConstuctorParams = constructorParams }));
                }

                viewModelProperties.AppendLine("public " + GetFullName(property.PropertyType) + " " + property.Name + " { get; set; }");
            }

            foreach (var property in addedProperties)
            {
                foreach (var customAttribute in property.CustomAttributes)
                    viewModelProperties.AppendLine(GenerateAttributeCode(customAttribute));

                viewModelProperties.AppendLine("public " + GetFullName(property.Type) + " " + property.Name + " { get; set; }");
            }

            var classCode = new StringBuilder();


            classCode.AppendLine("public " + Name + "() {}");
            classCode.AppendLine("public " + Name + "(" + GetFullName(modelType) + " model) : base(model) {}");
            classCode.AppendLine();

            classCode.AppendLine(viewModelProperties.ToString());

            var result = new StringBuilder();

            result.AppendLine("public class " + Name + " : ViewModel<" + GetFullName(modelType) + ">");
            result.AppendLine("{");

            result.AppendLine(Indent(classCode.ToString()));

            result.AppendLine("}");

            return Indent(result.ToString());
        }

        private string GetFullName(Type type)
        {
            if (type.IsGenericType)
            {
                var genericParams = string.Join(", ", type.GetGenericArguments().Select(gt => GetFullName(gt)));
                return type.Namespace + "." + type.Name.Remove(type.Name.IndexOf('`')) + "<" + genericParams + ">";
            }
            else
                return type.FullName;
        }
    }
}
